
<template>
  <div
    class="virtual-selector"
    :id="vsId"
  >
    <span
      class="virtual-selector__label"
      :class="{none: !label}"
    >{{ label }}</span>
    <div class="virtual-selector__input-wrapper">
      <input
        class="virtual-selector__input"
        :placeholder="placeholder"
        v-model="selected[option.itemNameKey]"
        @keyup="handleKeyup"
        @input="handleInput"
        @focus="handleFocus($event)"
        @select="handleInputSelect($event)"
      />
      <i class="virtual-selector__arrow">
        <svg
          viewBox="64 64 896 896"
          data-icon="down"
          width="1em"
          height="1em"
          fill="currentColor"
          aria-hidden="true"
          focusable="false"
          class=""
        >
          <path
            d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path>
        </svg>
      </i>
      <div
        v-if="loading"
        class="virtual-selector__loading"
      >
        <slot name="loading"></slot>
      </div>
      <div
        v-if="flist.length===0"
        class="virtual-selector__dropdown"
        style="text-align: center;"
      >
        <slot name="nodata"></slot>
      </div>
      <recycle-scroller
        v-if="flist.length > 0"
        class="virtual-selector__scroller virtual-selector__dropdown"
        :items="flist"
        :item-size="itemSize"
        :key-field="option.itemNameKey"
        v-slot="{ item }"
      >
        <div
          class="virtual-selector__dropdown-item"
          :class="{
            'virtual-selector__dropdown-item--selected':
              item[option.itemValueKey] === selected[option.itemValueKey],
          }"
          @click="handleItemSelect($event, item)"
        >
          <slot
            v-if="$scopedSlots.item"
            name="item"
            :item="item"
          ></slot>
          <slot v-else>{{ item[option.itemNameKey] }}</slot>
        </div>
      </recycle-scroller>
    </div>
  </div>
</template>

<script>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

function debounce(fn, delay) {
  let timer

  return function() {
    const context = this
    const args = arguments

    clearTimeout(timer)

    timer = setTimeout(function() {
      fn.apply(context, args)
    }, delay)
  }
}

const defaultItemPageSize = 8
const defaultItemGap = 0
const dropdownActiveClassName = 'virtual-selector__input-wrapper--active'

export default {
  name: 'VirtualSelector',
  components: { RecycleScroller },
  props: {
    loading: {
      type: Boolean,
      default: false
    },
    label: {
      type: String,
      default: ''
    },
    placeholder: {
      type: String,
      default: ''
    },
    value: {
      type: Object,
      default: () => {
      }
    },
    list: {
      type: Array,
      required: true,
      default: () => []
    },
    /**
     * option: {
     *   itemNameKey: string,
     *   itemValueKey: string,
     *   itemPageSize: number
     *   itemGap: number
     * }
     */
    option: {
      type: Object,
      required: true,
      default: () => ({
        itemNameKey: 'label',
          itemValueKey: 'key',
          itemPageSize: 8,
          itemGap: 5
      })
    }
  },
  data() {
    return {
      id: new Date().getTime(),
      flist: [],
      selected: {},
      itemSize: 32 + ((this.option && this.option.itemGap) || defaultItemGap)
    }
  },
  computed: {
    vsId() {
      return `virtual-selector-${this.id}`
    }
  },
  watch: {
    list: {
      immediate: true,
      handler() {
        this.init()
      }
    }
  },
  methods: {
    init() {
      if (!this.list || this.list.length == 0) return
      if (
        !this.option ||
        !this.option.itemNameKey ||
        !this.option.itemValueKey
      ) {
        throw new Error(
          '请指定列表选项“itemNameKey”或“itemValueKey”'
        )
      }
      this.flist = [...this.list]
      this.value instanceof Object &&
      (this.selected = {
        [this.option.itemNameKey]: this.value[this.option.itemNameKey],
        [this.option.itemValueKey]: this.value[this.option.itemValueKey]
      })
      this.$nextTick(() => {
        document.getElementById(this.vsId).querySelector('.virtual-selector__scroller').style.maxHeight =
          (this.option.itemPageSize || defaultItemPageSize) * this.itemSize +
          4 +
          'px'
      })
    },
    mount() {
      document.addEventListener('click', this.handleGlobalClick, false)
    },
    unmount() {
      document.removeEventListener('click', this.handleGlobalClick, false)
    },
    handleKeyup: debounce(function() {
      //防抖监控ipnut输入，并组装需要显示的内容
      const input = this.selected[this.option.itemNameKey]
      this.option.itemNameKey !== this.option.itemValueKey && (this.selected[this.option.itemValueKey] = '')
      if (!input) {
        this.flist = [...this.list]
      } else {
        this.flist = this.list.filter((item) => {
          if (item[this.option.itemNameKey].toLowerCase() === input.toLowerCase()) {
            this.selected[this.option.itemValueKey] = item[this.option.itemValueKey]
            this.$nextTick(() => {
              this.$emit('select', {
                id: this.vsId,
                select: { ...this.selected }
              })
            })
          }
          return item[this.option.itemNameKey].toString().toLowerCase().includes(input.toLowerCase())
        })
      }
      this.$emit('search', {
        id: this.vsId,
        search: { [this.option.itemNameKey]: input }
      })
    }, 300),
    handleInput() {
      //通知上层封装，input输入了什么
      this.$emit('input', {
        [this.option.itemNameKey]: this.selected[this.option.itemNameKey]
      })
    },
    handleFocus(e) {
      //通知上层封装，当前点击的组件ID，它是那个
      e.target.offsetParent.classList.toggle(dropdownActiveClassName)
      this.$emit('focus', {
        id: this.vsId,
        focus: { event: e }
      })
    },
    handleInputSelect(e) {
      //框选input中的内容
      e.target.offsetParent.classList.add(dropdownActiveClassName)
    },
    handleItemSelect(e, item) {
      //返回用户选的option
      this.selected = {
        ...item,
        [this.option.itemNameKey]: e.target.offsetParent.innerText
      }
      this.$emit('select', {
        id: this.vsId,
        select: { ...this.selected }
      })
    },
    handleGlobalClick(e) {
      //监听页面点击事件，如果点击的不是组件，则清空组件的所有input 样式、关闭虚拟滚动弹窗
      if (e.target.className === 'virtual-selector__input') return
      Array.from(document.querySelectorAll('.virtual-selector')).forEach((el) => {
          el.querySelector('.virtual-selector__input-wrapper').classList.remove(
            dropdownActiveClassName
          )
        }
      )
    }
  },
  mounted: function() {
    //挂载页面click监听
    this.mount()
  },
  beforeDestroy: function() {
    //卸载页面ckick监听
    this.unmount()
  }
}
</script>

<style scoped>
.virtual-selector {
  display: inline-flex;
  align-items: center;
  box-sizing: border-box;
}

.virtual-selector__label {
  line-height: 1.5;
  font-size: 14px;
  white-space: nowrap;
  color: #333;
}

.virtual-selector__label::after {
  position: relative;
  top: -0.5px;
  content: ":";
  margin: 0 8px 0 2px;
}

.virtual-selector__label.none::after {
  display: none;
}

.virtual-selector__input-wrapper {
  position: relative;
  flex: 1;
}

.virtual-selector__input {
  display: block;
  width: 100%;
  height: 32px;
  padding: 0 34px 0 11px;
  border: 1px solid #DCDFE6;
  border-radius: 5px;
  background-color: #fff;
  color: rgba(0, 0, 0, 0.65);
  font-size: 14px;
  box-sizing: border-box;
  outline: none;
  transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  cursor: text;
}

.virtual-selector__input::placeholder,
.virtual-selector__input::-webkit-input-placeholder {
  color: rgba(0, 0, 0, 0.65);
}

.virtual-selector__input:hover {
  border-color: #346fdf;
}

.virtual-selector__arrow {
  position: absolute;
  top: 10px;
  right: 11px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 12px;
  height: 12px;
  transition: transform 0.3s, -webkit-transform 0.3s;
  pointer-events: none;
}

.virtual-selector__arrow svg {
  color: rgba(0, 0, 0, 0.25);
}

.virtual-selector__loading {
  position: absolute;
  top: 1px;
  left: 1px;
  width: calc(100% - 2px);
  height: 30px;
  line-height: 30px;
  font-size: 12px;
  text-align: center;
  color: rgba(0, 0, 0, 0.65);
  background-color: #fff;
}

.virtual-selector__input-wrapper--active input {
  border-color: #409eff;
  /*box-shadow: 0 0 0 2px rgba(15, 72, 179, 0.2);*/
}

.virtual-selector__input-wrapper--active .virtual-selector__arrow {
  transform: rotate(180deg);
}

.virtual-selector__input-wrapper--active .virtual-selector__dropdown {
  display: block;
}

.virtual-selector__dropdown {
  display: none;
  position: absolute;
  min-width: 100%;
  padding: 4px 0;
  margin: 5px 0 0;
  border-radius: 5px;
  box-sizing: border-box;
  line-height: 1.5;
  list-style: none;
  font-size: 14px;
  background-color: #fff;
  color: rgba(0, 0, 0, 0.65);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  outline: none;
  z-index: 1050;
  overflow-y: auto;
  transform: translateZ(0px);
}

.virtual-selector__scroller {
  max-height: 252px;
}
/**/
.virtual-selector__scroller::-webkit-scrollbar {
   width: 12px;
   height: 6px;
   background: transparent;
 }

.virtual-selector__scroller::-webkit-scrollbar-thumb {
   background: transparent;
   border-radius: 5px;
 }

.virtual-selector__scroller::-webkit-scrollbar-thumb {
   background: hsla(0, 0%, 53%, 0.4);
 }

.virtual-selector__scroller::-webkit-scrollbar-track {
   background: hsla(0, 0%, 53%, 0.1);
 }


/**/



.virtual-selector__dropdown-item {
  display: block;
  padding: 5px 12px;
  line-height: 22px;
  font-weight: 400;
  font-size: 14px;
  color: rgba(0, 0, 0, 0.65);
  text-align: left;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  cursor: pointer;
  transition: background 0.3s ease;
}

.virtual-selector__dropdown-item:hover {
  background-color: #dae7f2;
}

.virtual-selector__dropdown-item--selected {
  font-weight: 600;
  color: rgba(64, 158, 255, 1);
  background-color: #fafafa;
}
</style>